import {JSJAC_TIMERVAL,JSJAC_CHECKQUEUEINTERVAL,
  JSJAC_ALLOW_SCRAM,
  JSJAC_INACTIVITY,JSJAC_ALLOW_PLAIN, JSJAC_CHECKINQUEUEINTERVAL, JSJAC_ERR_COUNT, JSJAC_RETRYDELAY, JSJAC_REGID_TIMEOUT} from './JSJaCConfig'
import {JSJaCPacket} from './JSJaCPacket';
import {JSJaC} from './JSJaC';
import {JSJaCCookie} from './JSJaCCookie';
import {JSJaCKeys} from './JSJaCKeys';
import {JSJaCUtils} from './JSJaCUtils';
import {JSJaCError} from './JSJaCError';
import {JSJaCIQ} from './JSJaCIQ';
import { NS_REGISTER, NS_AUTH, NS_BIND, NS_SESSION, NS_STREAM, NS_SASL } from './JSJaCConstants';
import {Crypt} from 'c3-util'
import {JSJaCPresence} from './JSJaCPresence';
import {JSJaCMessage} from './JSJaCMessage';
import  {IConsoleLogger, LOG_LEVEL } from './IConsoleLogger';
import {IXMLDocument} from './IXMLDocument';
import {IElement} from './IElement';
import {XmlUtil} from './XmlUtil';
import {JSJaCConsoleLogger}  from './JSJaCConsoleLogger';
/**
 * @fileoverview Contains all things in common for all subtypes of connections
 * supported.
 * @author Stefan Strigler steve@zeank.in-berlin.de
 */

/*exported JSJaCConnection */

/**
  
 * @param {Object} oArg The configuration to be used for connecting.
 * @param {string} oArg.domain The domain name of the XMPP service.
 * @param {string} oArg.username The username (nodename) to be logged in with.
 * @param {string} oArg.resource The resource to identify the login with.
 * @param {string} oArg.password The user's password.
 * @param {string} [oArg.authzid] Authorization identity. Used to act as another user, in most cases not needed and rarely supported by servers. If present should be a bare JID (user@example.net).
 * @param {boolean} [oArg.allow_plain] Whether to allow plain text logins.
 * @param {boolean} [oArg.allow_scram] Whether to allow SCRAM-SHA-1 authentication. Please note that it is quite slow, do some testing on all required browsers before enabling.
 * @param {boolean} [oArg.register] Whether to register a new account.
 * @param {string} [oArg.host] The host to connect to which might be different from the domain given above. So some XMPP service might host the domain 'example.com' but might be located at the host 'jabber.example.com'. Normally such situations should be gracefully handled by using DNS SRV records. But in cases where this isn't available you can set the host manually here.
 * @param {int} [oArg.port] The port of the manually given host from above.
 * @param {string} [oArg.authhost] The host that handles the actualy authorization. There are cases where this is different from the settings above, e.g. if there's a service that provides anonymous logins at 'anon.example.org'.
 * @param {string} [oArg.authtype] Must be one of 'sasl' (default), 'nonsasl', 'saslanon', or 'anonymous'.
 * @param {string} [oArg.xmllang] The requested language for this login. Typically XMPP server try to respond with error messages and the like in this language if available.
 */
export interface LoginParam {
   domain?:string,
   username:string,
   resource?:string,
   password?:string,
   pass?:string
   authzid?:string,
   allow_plain?:boolean,
   allow_scram?:boolean,
   register?:boolean,
   host?:string,
   port?:number,
   authhost?:string,
   xmllang?:string,
   authtype?:string
}
export interface ConnectionParam {
  httpbase:string,
  oDbg?:IConsoleLogger,
  timerval?:number,
  cookie_prefix?:string,
}
/**
 * Creates a new Jabber/XMPP connection (a connection to a jabber server)
 * @class Somewhat abstract base class for jabber connections. Contains all
 * of the code in common for all jabber connections
 * @constructor
 * @param {Object} oArg Configurational object for this connection.
 * @param {string} oArg.httpbase The connection endpoint of the HTTP service to talk to.
 * @param {JSJaCDebugger} [oArg.oDbg] A reference to a debugger implementing the JSJaCDebugger interface.
 * @param {int} [oArg.timerval] The polling interval.
 * @param {string} [oArg.cookie_prefix] Prefix to cookie names used when suspending.
 */
export abstract class  JSJaCConnection {
  protected _httpbase:string 
  protected oDbg:IConsoleLogger;
  protected _cookie_prefix:string=''
  protected _connected:boolean=false
  private _keys = null;
  protected _ID:number = 0;
  private _inQ:Array<IElement> = [];
  private _pQueue = [];
  private _regIDs = [];
  private _req = [];
  protected _status:string = 'intialized';
  private _errcnt:number = 0;
  private _inactivity = JSJAC_INACTIVITY;
  private _sendRawCallbacks = [];
  public domain:string
  public username:string
  public resource:string
  public pass:string;
  public authzid:string
  public register:boolean
  public authhost:string
  public authtype:string
  
  private _xmllang:string
  
  private _allow_plain:boolean=false
  private _allow_scram:boolean
  
  public host:string
  public port:number
  public jid:string
  public fulljid:string
  private _rid:number

  private _events:any[]=[];

  private _timerval:number
  
  private _keys2:JSJaCKeys

  protected _interval:number
  protected _inQto:number

  protected _timeout:number

  private has_sasl:boolean=false;

  private mechs={}

  protected streamid:string;

  private _saltedPassword:string;
  private _clientFirstMessageBare:string

  private _authMessage:string;
  private _nonce:string;

  private _realm:string;
  private _digest_uri:string;
  private _cnonce:string;
  private _nc:string;

  constructor(oArg:ConnectionParam) {
    this._httpbase=oArg.httpbase
    if(oArg.oDbg) {
      /**
       * Reference to debugger interface
       * (needs to implement method <code>log</code>)
       * @type JSJaCDebugger
       */
      this.oDbg=oArg.oDbg
    } else {
      this.oDbg =new JSJaCConsoleLogger(LOG_LEVEL.DEBUG)
    }
    if (oArg.timerval)
      this.setPollInterval(oArg.timerval);
    else
      this.setPollInterval(JSJAC_TIMERVAL);
    if (oArg.cookie_prefix)
      this._cookie_prefix = oArg.cookie_prefix;

  }

  /**
   * @private
   */
  protected _setStatus(status?:string):void {
    if (!status || status === '')
      return;
    if (status != this._status) { // status changed!
      this._status = status;
      this._handleEvent('onstatuschanged', status);
      this._handleEvent('status_changed', status);
    }
  }
  /**
   * Connect to a jabber/XMPP server.
   * @param {Object} oArg The configuration to be used for connecting.
   * @param {string} oArg.domain The domain name of the XMPP service.
   * @param {string} oArg.username The username (nodename) to be logged in with.
   * @param {string} oArg.resource The resource to identify the login with.
   * @param {string} oArg.password The user's password.
   * @param {string} [oArg.authzid] Authorization identity. Used to act as another user, in most cases not needed and rarely supported by servers. If present should be a bare JID (user@example.net).
   * @param {boolean} [oArg.allow_plain] Whether to allow plain text logins.
   * @param {boolean} [oArg.allow_scram] Whether to allow SCRAM-SHA-1 authentication. Please note that it is quite slow, do some testing on all required browsers before enabling.
   * @param {boolean} [oArg.register] Whether to register a new account.
   * @param {string} [oArg.host] The host to connect to which might be different from the domain given above. So some XMPP service might host the domain 'example.com' but might be located at the host 'jabber.example.com'. Normally such situations should be gracefully handled by using DNS SRV records. But in cases where this isn't available you can set the host manually here.
   * @param {int} [oArg.port] The port of the manually given host from above.
   * @param {string} [oArg.authhost] The host that handles the actualy authorization. There are cases where this is different from the settings above, e.g. if there's a service that provides anonymous logins at 'anon.example.org'.
   * @param {string} [oArg.authtype] Must be one of 'sasl' (default), 'nonsasl', 'saslanon', or 'anonymous'.
   * @param {string} [oArg.xmllang] The requested language for this login. Typically XMPP server try to respond with error messages and the like in this language if available.
   */
  connect(oArg:LoginParam):void {
      this._setStatus('connecting');
  
      this.domain = oArg.domain || 'localhost';
      this.username = oArg.username;
      this.resource = oArg.resource;
      this.pass = oArg.password || oArg.pass;
      this.authzid = oArg.authzid || '';
      this.register = oArg.register;
  
      this.authhost = oArg.authhost || oArg.host || oArg.domain;
      this.authtype = oArg.authtype || 'sasl';
  
      if (oArg.xmllang && oArg.xmllang !== '')
          this._xmllang = oArg.xmllang;
      else
          this._xmllang = 'en';
  
      if (oArg.allow_plain)
          this._allow_plain = oArg.allow_plain;
      else
          this._allow_plain = JSJAC_ALLOW_PLAIN;
  
      if (oArg.allow_scram)
          this._allow_scram = oArg.allow_scram;
      else
          this._allow_scram = JSJAC_ALLOW_SCRAM;
  
      this.host = oArg.host;
      this.port = oArg.port || 5222;
  
      this.jid = this.username + '@' + this.domain;
      this.fulljid = this.jid + '/' + this.resource;
  
      this._rid  = Math.round( 100000.5 + ( ( (900000.49999) - (100000.5) ) * Math.random() ) );
  
      // setupRequest must be done after rid is created but before first use in reqstr
      let slot:number = this._getFreeSlot();
      if(slot<0)
         return ;
      this._req[slot] = this._setupRequest(true);
  
      var reqstr = this._getInitialRequestString();
  
      this.oDbg.debug(reqstr);
  
      this._req[slot].r.onreadystatechange =
          JSJaC.bind(function() {
              var r = this._req[slot].r;
              if (r.readyState == 4) {
                  this.oDbg.debug("async recv: "+r.responseText);
                  this._handleInitialResponse(r); // handle response
              }
          }, this);
  
      if (typeof(this._req[slot].r.onerror) != 'undefined') {
          this._req[slot].r.onerror =
              JSJaC.bind(function() {
                  this.oDbg.warn('XmlHttpRequest error');
              }, this);
      }
  
      this._req[slot].r.send(reqstr);
  };


  /**
   * @private
   */
  protected _handleEvent(event:string,arg?:string|JSJaCPacket|JSJaCError):void {
    event = event.toLowerCase(); // don't be case-sensitive here
    this.oDbg.info("incoming event '"+event+"'");
    if (!this._events[event])
      return;
    this.oDbg.info("handling event '"+event+"'");
    for (let i=0;i<this._events[event].length; i++) {
      var aEvent = this._events[event][i];
      if (typeof aEvent.handler == 'function') {
        if (arg) {
          if (arg instanceof JSJaCPacket) { // it's a packet
            if ((!arg.getNode().hasChildNodes() && aEvent.childName != '*') ||
                (arg.getNode().hasChildNodes() &&
                 !arg.getChild(aEvent.childName, aEvent.childNS)))
              continue;
              //TODO 这里逻辑有问题,event 是string型
            if (aEvent.type != '*' &&
                arg.getType() != aEvent.type)
              continue;
            this.oDbg.debug(aEvent.childName+"/"+aEvent.childNS+"/"+aEvent.type+" => match for handler "+aEvent.handler);
          }
          if (aEvent.handler(arg)) {
            // handled!
            break;
          }
        } else if (aEvent.handler()) {
          // handled!
          break;
        }
      }
    }
  };

  /**
   * Sets polling interval for this connection
   * @param {int} timerval Milliseconds to set timer to
   * @return effective interval this connection has been set to
   * @type int
   */
  setPollInterval(timerval:number):number {
    this._timerval = timerval;
    return this._timerval;
  };

  protected abstract _getFreeSlot():number;

  protected abstract _setupRequest(async:boolean):any;

  protected abstract _getInitialRequestString():string;


  protected abstract _getRequestString(raw:string, last:boolean):string

  protected abstract _reInitStream(cb:Function):void

  protected abstract _parseResponse(r:any):IXMLDocument
  /**
   * Tells whether this connection is connected
   * @return <code>true</code> if this connections is connected,
   * <code>false</code> otherwise
   * @type boolean
   */
  connected():boolean {
     return this._connected;
     }

  /**
   * Disconnects from jabber server and terminates session (if applicable)
   */
  disconnect():void {
    this._setStatus('disconnecting');
  
    if (!this.connected())
      return;
    this._connected = false;
  
    clearInterval(this._interval);
    clearInterval(this._inQto);
  
    if (this._timeout)
      clearTimeout(this._timeout); // remove timer
  
    let slot = this._getFreeSlot();
    // Intentionally synchronous
    this._req[slot] = this._setupRequest(false);
  
    let request = this._getRequestString('', true);
  
    this.oDbg.debug("Disconnecting: " + request);
    try {
      this._req[slot].r.send(request);
    } catch(e) {}
    this.oDbg.debug("disconnected");
    try {
      JSJaCCookie.read(this._cookie_prefix+'JSJaC_State').erase();
    } catch (e) {}
  
    this._handleEvent('ondisconnect');
  };

/**
 * Registers an event handler (callback) for this connection.

 * <p>Note: All of the packet handlers for specific packets (like
 * message_in, presence_in and iq_in) fire only if there's no
 * callback associated with the id.<br>

 * <p>Example:<br/>
 * <code>con.registerHandler('iq', 'query', 'jabber:iq:version', handleIqVersion);</code>


 * @param {String} event One of

 * <ul>
 * <li>onConnect - connection has been established and authenticated</li>
 * <li>onDisconnect - connection has been disconnected</li>
 * <li>onResume - connection has been resumed</li>

 * <li>onStatusChanged - connection status has changed, current
 * status as being passed argument to handler. See {@link #status}.</li>

 * <li>onError - an error has occured, error node is supplied as
 * argument, like this:<br><code>&lt;error code='404' type='cancel'&gt;<br>
 * &lt;item-not-found xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/&gt;<br>
 * &lt;/error&gt;</code></li>

 * <li>packet_in - a packet has been received (argument: the
 * packet)</li>

 * <li>packet_out - a packet is to be sent(argument: the
 * packet)</li>

 * <li>message_in | message - a message has been received (argument:
 * the packet)</li>

 * <li>message_out - a message packet is to be sent (argument: the
 * packet)</li>

 * <li>presence_in | presence - a presence has been received
 * (argument: the packet)</li>

 * <li>presence_out - a presence packet is to be sent (argument: the
 * packet)</li>

 * <li>iq_in | iq - an iq has been received (argument: the packet)</li>
 * <li>iq_out - an iq is to be sent (argument: the packet)</li>
 * </ul>
 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.

 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet [optional]

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given) [optional]

 * @param {String} type The type of the packet to handle (works only if childName and chidNS are given (both may be set to '*' in order to get skipped) [optional]


 * @return This object
 */
registerHandler(event:string,handler:Function,childName:string=undefined,childNS:string=undefined,
  type:string=undefined):JSJaCConnection {
  event = event.toLowerCase(); // don't be case-sensitive here
  var eArg = {handler: handler,
              childName: childName||'*',
              childNS: childNS||'*',
              type: type||'*'};
  if (!this._events[event])
    this._events[event] = [eArg];
  else
    this._events[event] = this._events[event].concat(eArg);

  // sort events in order how specific they match criterias thus using
  // wildcard patterns puts them back in queue when it comes to
  // bubbling the event
  this._events[event] = this._events[event].sort(function(a,b) {
    var aRank = 0;
    var bRank = 0;

    if (a.type == '*')
      aRank++;
    if (a.childNS == '*')
      aRank++;
    if (a.childName == '*')
      aRank++;
    if (b.type == '*')
      bRank++;
    if (b.childNS == '*')
      bRank++;
    if (b.childName == '*')
      bRank++;

    if (aRank > bRank)
      return 1;
    if (aRank < bRank)
      return -1;
    return 0;
  });
  this.oDbg.info("registered handler for event '"+event+"'");

  return this;
};

unregisterHandler(event:string,handler:Function):JSJaCConnection {
  event = event.toLowerCase(); // don't be case-sensitive here

  if (!this._events[event])
    return this;

  var arr = this._events[event], res = [];
  for (var i=0; i<arr.length; i++)
    if (arr[i].handler != handler)
      res.push(arr[i]);

  if (arr.length != res.length) {
    this._events[event] = res;
    this.oDbg.info("unregistered handler for event '"+event+"'");
  }

  return this;
};
/**
 * Register for iq packets of type 'get'.
 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given)

 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.

 * @return This object
 */
registerIQGet(childName:string, childNS:string, handler:Function):JSJaCConnection {
  return this.registerHandler('iq', handler, childName, childNS, 'get');
};
/**
 * Register for iq packets of type 'set'.
 * @param {String} childName A childnode's name that must occur within a
 * retrieved packet

 * @param {String} childNS A childnode's namespace that must occure within
 * a retrieved packet (works only if childName is given)

 * @param {Function} handler The handler to be called when event occurs. If your handler returns 'true' it cancels bubbling of the event. No other registered handlers for this event will be fired.

 * @return This object
 */
registerIQSet (childName:string, childNS:string, handler:Function):JSJaCConnection {
  return this.registerHandler('iq', handler, childName, childNS, 'set');
};


/**
 * Tries to transform a w3c DOM node to JSJaC's internal representation
 * (JSJaCPacket type, one of JSJaCPresence, JSJaCMessage, JSJaCIQ)
 * @param: {Node
 * http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247}
 * node The node to be transformed
 * @return A JSJaCPacket representing the given node. If node's root
 * elemenent is not one of 'message', 'presence' or 'iq',
 * <code>null</code> is being returned.
 * @type JSJaCPacket
 */
 protected wrapNode(node:IElement) {
  let oPacket:JSJaCPacket;

  switch (node.getLocalName().toLowerCase()) {
  case 'presence':
      oPacket = new JSJaCPresence();
      break;
  case 'message':
      oPacket = new JSJaCMessage();
      break;
  case 'iq':
      oPacket = new JSJaCIQ();
      break;
  default:
    oPacket=null
  }
  if (oPacket) {
    oPacket.setDoc(XmlUtil.loadXML(node.toXmlStr()))
  }

  return oPacket;
};

  /**
   * 
   */
  private _process(timerval?:number):void {
    if (!this.connected()) {
      this.oDbg.warn("Connection lost ...");
      if (this._interval)
        clearInterval(this._interval);
      return;
    }
  
    this.setPollInterval(timerval);
  
    if (this._timeout)
      clearTimeout(this._timeout);
  
    var slot = this._getFreeSlot();
  
    if (slot < 0)
      return;
  
    if (typeof(this._req[slot]) != 'undefined' &&
        typeof(this._req[slot].r) != 'undefined' &&
        this._req[slot].r.readyState != 4) {
      this.oDbg.debug("Slot "+slot+" is not ready");
      return;
    }
  
    if (this._pQueue.length === 0 &&
        this._req[(slot+1)%2] && this._req[(slot+1)%2].r.readyState != 4) {
      this.oDbg.info("all slots busy, standby ...");
      return;
    }
  
    this._req[slot] = this._setupRequest(true);
  
    /* setup onload handler for async send */
    this._req[slot].r.onreadystatechange =
    JSJaC.bind(function() {
                 if (!this.connected())
                   return;
                 if (this._req[slot].r.readyState == 4) {
                   this.oDbg.debug("async recv: "+this._req[slot].r.responseText);
                   this._handleResponse(this._req[slot]);
                   // schedule next tick
                   this._setStatus('processing');
                   if (this._pQueue.length) {
                     this._timeout = setTimeout(JSJaC.bind(this._process, this),
                                                100);
                   } else {
                     this.oDbg.debug("scheduling next poll in "+
                                   this.getPollInterval()+
                                   " msec");
                     this._timeout = setTimeout(JSJaC.bind(this._process, this),
                                                this.getPollInterval());
                   }
                 }
               }, this);
  
    try {
      this._req[slot].r.onerror =
        JSJaC.bind(function() {
                     if (!this.connected())
                       return;
                     this._errcnt++;
                     this.oDbg.warn('XmlHttpRequest error ('+this._errcnt+')');
                     if (this._errcnt > JSJAC_ERR_COUNT) {
                       // abort
                       this._abort();
                       return;
                     }
  
                     this._setStatus('onerror_fallback');
  
                     // schedule next tick
                     setTimeout(JSJaC.bind(this._repeat, this), JSJAC_RETRYDELAY);
                     return;
                   }, this);
    } catch(e) {
        // well ... no onerror property available, maybe we
        // can catch the error somewhere else ...
    }
  
    var reqstr = this._getRequestString('',false);
  
    if (typeof(this._rid) != 'undefined') // remember request id if any
      this._req[slot].rid = this._rid;
  
    this.oDbg.debug("sending: " + reqstr);
    this._req[slot].r.send(reqstr);
};

    /**
     * 
     */
 protected _handlePID(packet:JSJaCPacket):boolean {
   if (!packet.getID())
     return false;
 
   var jid = packet.getFrom() || this.jid;
 
   if (packet.getFrom() == this.domain) 
     jid = this.jid;
 
   var id = packet.getID();
   if (this._regIDs[jid] && this._regIDs[jid][id]) {
     this.oDbg.info("handling id "+id);
     var reg = this._regIDs[jid][id];
     if (reg.cb.call(this, packet, reg.arg) === false) {
       // don't unregister
       return false;
     } else {
       delete this._regIDs[jid][id];
       return true;
     }
   } else {
     this.oDbg.warn("not handling id '"+id+"' from jid "+jid);
     return false;
   }
 };
    /**
     * Sends a JSJaCPacket
     * @param {JSJaCPacket} packet  The packet to send
     * @param {Function}    cb      The callback to be called if there's a reply
     * to this packet (identified by id) [optional]
     * @param {Object}      arg     Arguments passed to the callback
     * (additionally to the packet received) [optional]
     * @return 'true' if sending was successfull, 'false' otherwise
     * @type boolean
     */
    send(packet:JSJaCPacket,cb:Function,arg?:any):boolean {
      if (!packet || !packet.pType) {
        this.oDbg.warn("no packet: "+packet);
        return false;
      }
  
      if (!this.connected())
        return false;
  
      // if (this._xmllang && !packet.getXMLLang())
      //   packet.setXMLLang(this._xmllang);
  
      // remember id for response if callback present
      if (cb) {
        if (!packet.getID())
          packet.setID('JSJaCID_'+this._ID++); // generate an ID
  
        // register callback with id
        this._registerPID(packet, cb, arg);
      }
  
      this._pQueue = this._pQueue.concat(packet.xml());
      this._handleEvent(packet.pType()+'_out', packet);
      this._handleEvent("packet_out", packet);
  
      return true;
    };

    /**
     * @private
     * @param {JSJaCPacket} packet The packet to be sent.
     * @param {function} cb The callback to be called when response is received.
     * @param {any} arg Optional arguments to be passed to 'cb' when executing it.
     * @return Whether registering an ID was successful
     * @type boolean
     */
    protected _registerPID(packet:JSJaCPacket, cb?:Function, arg?:any):boolean {
      if (!cb || typeof cb != 'function') {
        this.oDbg.warn("callback is not a function");
        return false;
      }
      this.oDbg.debug("registering id for packet "+packet.xml());
      var id = packet.getID();
      if (!id) {
        this.oDbg.warn("id missing");
        return false;
      }
    
      var jid = packet.getTo() || this.jid;
      
      if (packet.getTo() == this.domain)
         jid = this.jid;
    
      if (!this._regIDs[jid]) {
        this._regIDs[jid] = {};
      }
    
      if (this._regIDs[jid][id] != null) {
        this.oDbg.debug("id already registered: " + id);
        return false;
      }
      this._regIDs[jid][id] = {
          cb:  cb,
          arg: arg,
          ts:  JSJaCUtils.now()
      };
      this.oDbg.debug("registered id "+id);
      this._cleanupRegisteredPIDs();
      return true;
    };
  
    /**
     * Sends an IQ packet. Has default handlers for each reply type.
     * Those maybe overriden by passing an appropriate handler.
     * @param {JSJaCIQPacket} iq - the iq packet to send
     * @param {Object} handlers - object with properties 'error_handler',
     *                            'result_handler' and 'default_handler'
     *                            with appropriate functions
     * @param {Object} arg - argument to handlers
     * @return 'true' if sending was successfull, 'false' otherwise
     * @type boolean
     */
    sendIQ(iq:JSJaCPacket, handlers:any, arg:any):boolean {
      if (!iq || iq.pType() != 'iq') {
        return false;
      }
  
      handlers = handlers || {};
      let error_handler = handlers.error_handler || JSJaC.bind(function(aIq:JSJaCPacket) {
            this.oDbg.error(aIq.xml());
        }, this);
  
      let result_handler = handlers.result_handler ||  JSJaC.bind(function(aIq:JSJaCPacket) {
            this.oDbg.info(aIq.xml(), 2);
        }, this);
  
      let iqHandler = function(aIq:JSJaCPacket, arg:any) {
        switch (aIq.getType()) {
          case 'error':
          error_handler(aIq);
          break;
          case 'result':
          result_handler(aIq, arg);
          break;
        }
      };
      return this.send(iq, iqHandler, arg);
    };

    private _cleanupRegisteredPIDs() {
      let now:number = JSJaCUtils.now();
      for (let jid in this._regIDs) {
        if (this._regIDs.hasOwnProperty(jid)) {
          for (let id in this._regIDs[jid]) {
            if (this._regIDs[jid].hasOwnProperty(id)) {
              if (this._regIDs[jid][id].ts + JSJAC_REGID_TIMEOUT < now) {
                this.oDbg.warn("deleting registered id '"+id+ "' due to timeout");
                delete this._regIDs[jid][id];
              }
            }
          }
        }
      }
    };
  /**
   * Returns current status of this connection
   * @return String to denote current state. One of
   * <ul>
   * <li>'initializing' ... well
   * <li>'connecting' if connect() was called
   * <li>'resuming' if resume() was called
   * <li>'processing' if it's about to operate as normal
   * <li>'onerror_fallback' if there was an error with the request object
   * <li>'protoerror_fallback' if there was an error at the http binding protocol flow (most likely that's where you interested in)
   * <li>'internal_server_error' in case of an internal server error
   * <li>'suspending' if suspend() is being called
   * <li>'aborted' if abort() was called
   * <li>'disconnecting' if disconnect() has been called
   * </ul>
   * @type String
   */
  status():string { return this._status; };
  
  /**
   * 
   */
  private _abort() {
    clearTimeout(this._timeout); // remove timer
  
    clearInterval(this._inQto);
    clearInterval(this._interval);
  
    this._connected = false;
  
    this._setStatus('aborted');
  
    this.oDbg.warn("Disconnected.");
    this._handleEvent('ondisconnect');
    this._handleEvent('onerror',
                      new JSJaCError('500','cancel','service-unavailable'));
  };
  
  
  /**
   * 
   */
  protected _doAuth() {
    if (this.has_sasl && this.authtype == 'nonsasl')
      this.oDbg.warn("Warning: SASL present but not used");
  
    if (!this._doSASLAuth() &&
        !this._doLegacyAuth()) {
      this.oDbg.warn("Auth failed for authtype "+this.authtype);
      this.disconnect();
      return false;
    }
    return true;
  };
  
  /**
   * @private
   */
  private _doSASLAuth() {
    if (this.authtype == 'nonsasl' || this.authtype == 'anonymous')
      return false;
  
    if (this.authtype == 'saslanon') {
      if (this.mechs['ANONYMOUS']) {
        this.oDbg.info("SASL using mechanism 'ANONYMOUS'");
        return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>",
        JSJaC.bind(this._doSASLAuthDone, this));
      }
      this.oDbg.warn("SASL ANONYMOUS requested but not supported");
    } else {
      if (this._allow_scram && this.mechs['SCRAM-SHA-1']) {
        this.oDbg.info("SASL using mechanism 'SCRAM-SHA-1'");
  
        this._clientFirstMessageBare = 'n=' + this.username.replace(/=/g, '=3D').replace(/,/g, '=2C') + ',r=' + JSJaCUtils.cnonce(16);
        var gs2Header;
        if (this.authzid) {
          gs2Header = 'n,a=' + this.authzid.replace(/=/g, '=3D').replace(/,/g, '=2C') + ',';
        } else {
          gs2Header = 'n,,';
        }
        var clientFirstMessage = gs2Header + this._clientFirstMessageBare;
  
        return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='SCRAM-SHA-1'>" +
                             Crypt.b64encode(clientFirstMessage) +
                             "</auth>",
                             JSJaC.bind(this._doSASLAuthScramSha1S1,this));
      } else if (this.mechs['DIGEST-MD5']) {
        this.oDbg.info("SASL using mechanism 'DIGEST-MD5'");
        return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='DIGEST-MD5'/>",
                             JSJaC.bind(this._doSASLAuthDigestMd5S1,this));
      } else if (this._allow_plain && this.mechs['PLAIN']) {
        this.oDbg.info("SASL using mechanism 'PLAIN'");
        var authStr = this.authzid+String.fromCharCode(0)+
        this.username+String.fromCharCode(0)+
        this.pass;
        this.oDbg.info("authenticating with '"+authStr+"'");
        authStr = Crypt.b64encode(authStr);
        return this._sendRaw("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>"+authStr+"</auth>",
                             JSJaC.bind(this._doSASLAuthDone, this));
      }
      this.oDbg.error("No SASL mechanism applied");
      this.authtype = 'nonsasl'; // fallback
    }
    return false;
  };
  /**
   * @private
   */
  protected _doInBandReg():void {
    if (this.authtype == 'saslanon' || this.authtype == 'anonymous')
      return; // bullshit - no need to register if anonymous
  
    /* ***
     * In-Band Registration see JEP-0077
     */
  
    let iq = new JSJaCIQ();
    iq.setType('set');
    iq.setID('reg1');
    iq.appendNode("query", {xmlns: NS_REGISTER},
                  [["username", this.username],
                   ["password", this.pass]]);
  
    this.send(iq,this._doInBandRegDone);
  };


  /**
   * @private
   */
   protected _sendRaw(xml:string,cb:Function,arg?:any):boolean {
    if (cb)
      this._sendRawCallbacks.push({fn: cb, arg: arg});
  
    this._pQueue.push(xml);
    this._process();
  
    return true;
  };

private getNodeName(el:any):string {
   if(el && el.nodeName){
     return el.nodeName
   } else if(el && el.getLocalName){
     return el.getLocalName()
   } else {
     return ''
   }

}

/**
 * 
 */
protected _doSASLAuthDone(el:any):void {
    let nodeName=this.getNodeName(el)
    if (nodeName != 'success') {
        this.oDbg.error("auth failed");
        this._handleEvent('onerror',new JSJaCError('401','auth','not-authorized'));
        this.disconnect();
    } else {
        this._reInitStream(JSJaC.bind(this._doStreamBind, this));
    }
};
  
  /**
   * @private
   */
  private _doInBandRegDone(iq:JSJaCIQ) {
    if (iq && iq.getType() == 'error') { // we failed to register
      this.oDbg.error("registration failed for "+this.username);
      this._handleEvent('onerror',iq.getChild('error'));
      return;
    }
  
    this.oDbg.info(this.username + " registered succesfully");
  
    this._doAuth();
  };
  
  /**
   * @private
   */
  private _doLegacyAuth():boolean {
    if (this.authtype != 'nonsasl' && this.authtype != 'anonymous')
      return false;
  
    /* ***
     * Non-SASL Authentication as described in JEP-0078
     */
    var iq = new JSJaCIQ();
    iq.setIQ(null,'get','auth1');
    iq.appendNode('query', {xmlns: NS_AUTH},
                  [['username', this.username]]);
  
    this.send(iq,this._doLegacyAuth2);
    return true;
  };

/**
 * @private
 */
private _doLegacyAuthDone(iq:JSJaCIQ):void {
  if (iq.getType() != 'result') { // auth' failed
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    this.disconnect();
  } else
    this._handleEvent('onconnect');
};

/**
 * @private
 */
private _doLegacyAuth2(resIq:JSJaCIQ):void {
  if (!resIq || resIq.getType() != 'result') {
    if (resIq && resIq.getType() == 'error')
      this._handleEvent('onerror',resIq.getChild('error'));
    this.disconnect();
    return;
  }

  let use_digest = (resIq.getChild('digest') !== null);

  /* ***
   * Send authentication
   */
  let iq:JSJaCIQ = new JSJaCIQ();
  iq.setIQ(null,'set','auth2');

  iq.appendNode('query', {xmlns: NS_AUTH},
                            [['username', this.username],
                             ['resource', this.resource]]);
  let query:IElement=iq.getChild('query');

  if (use_digest) { // digest login
    query.appendChild(iq.buildNode('digest', {xmlns: NS_AUTH},
                                   Crypt.hex_sha1(this.streamid + this.pass)));
  } else if (this._allow_plain) { // use plaintext auth
    query.appendChild(iq.buildNode('password', {xmlns: NS_AUTH},
                                    this.pass));
  } else {
    this.oDbg.warn("no valid login mechanism found");
    this.disconnect();
    return;
  }

  this.send(iq,this._doLegacyAuthDone);
};


/**
 * @private
 */
protected  _doSASLAuthScramSha1S2(el:Element) {
  if (el.nodeName != 'success') {
    this.oDbg.error('auth failed');
    this._handleEvent('onerror',new JSJaCError('401', 'auth', 'not-authorized'));
    this.disconnect();
  } else {
    let serverFinalMessage:string = Crypt.b64decode(el.firstChild.nodeValue);
    this.oDbg.info('got success: ' + serverFinalMessage);

    var data = {};
    var fields = serverFinalMessage.split(',');
    for(var field in fields) {
      if (fields.hasOwnProperty(field)) {
        var val = fields[field].substring(2);
        data[fields[field].substring(0, 1)] = val;
      }
    }

    var serverKey = Crypt.rstr_hmac_sha1(this._saltedPassword, 'Server Key');
    var serverSignature = Crypt.rstr_hmac_sha1(serverKey, Crypt.str2rstr_utf8(this._authMessage));
    var verifier = Crypt.b64decode_bin(data['v']);

    if(serverSignature !== verifier) {
      this.oDbg.error('server auth failed');
      this._handleEvent('onerror', new JSJaCError('401', 'auth', 'not-authorized'));
      this.disconnect();
    } else {
      this._reInitStream(JSJaC.bind(this._doStreamBind, this));
    }
  }
};

/**
 * @private
 */
protected _doSASLAuthScramSha1S1(el:Element) {
  if (el.nodeName != 'challenge') {
    this.oDbg.warn('challenge missing');
    this._handleEvent('onerror', new JSJaCError('401', 'auth', 'not-authorized'));
    this.disconnect();
  } else {
    var serverFirstMessage = Crypt.b64decode(el.firstChild.nodeValue);
    this.oDbg.info('got challenge: ' + serverFirstMessage);

    var data = {};
    var fields = serverFirstMessage.split(',');
    for(var field in fields) {
      if (fields.hasOwnProperty(field)) {
        var val = fields[field].substring(2);
        data[fields[field].substring(0, 1)] = val;
      }
    }

    var password = Crypt.str2rstr_utf8(this.pass);
    var u = Crypt.b64decode_bin(data['s']) + "\x00\x00\x00\x01";
    var h, i = parseInt(data['i'], 10);
    for(var j = 0; j < i; j++) {
      u = Crypt.rstr_hmac_sha1(password, u);
      h = JSJaCUtils.xor(h, u);
    }

    var gs2Header;
    if (this.authzid) {
      gs2Header = 'n,a=' + this.authzid.replace(/=/g, '=3D').replace(/,/g, '=2C') + ',';
    } else {
      gs2Header = 'n,,';
    }
    var clientFinalMessageWithoutProof = 'c=' + Crypt.b64encode(gs2Header) + ',r=' + data['r'];

    this._saltedPassword = h;
    var clientKey = Crypt.rstr_hmac_sha1(this._saltedPassword, 'Client Key');
    var storedKey = Crypt.rstr_sha1(clientKey);
    this._authMessage = this._clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof;
    var clientSignature = Crypt.rstr_hmac_sha1(storedKey, Crypt.str2rstr_utf8(this._authMessage));
    var proof = JSJaCUtils.xor(clientKey, clientSignature);

    var clientFinalMessage = clientFinalMessageWithoutProof + ',p=' + Crypt.rstr2b64(proof);

    this.oDbg.info('response: ' + clientFinalMessage);
    this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>" +
                  Crypt.b64encode(clientFinalMessage) +
                  "</response>",
                  this._doSASLAuthScramSha1S2);
  }
};

/**
 * @private
 */
protected _doSASLAuthDigestMd5S1(el:Element) {
  if (el.nodeName != "challenge") {
    this.oDbg.warn("challenge missing");
    this._handleEvent('onerror',new JSJaCError('401','auth','not-authorized'));
    this.disconnect();
  } else {
    var challenge = Crypt.b64decode(el.firstChild.nodeValue), index;
    this.oDbg.info("got challenge: "+challenge);

    index = challenge.indexOf("nonce=\"");
    if(index !== -1) {
      this._nonce = challenge.substring(index + 7);
      this._nonce = this._nonce.substring(0, this._nonce.indexOf("\""));
      this.oDbg.info("nonce: " + this._nonce);
    } else {
      this.oDbg.warn("no valid nonce found, aborting");
      this.disconnect();
      return;
    }

    index = challenge.indexOf("realm=\"");
    if (index !== -1) {
      this._realm = challenge.substring(index + 7);
      this._realm = this._realm.substring(0, this._realm.indexOf("\""));
    }
    this._realm = this._realm || this.domain;
    this.oDbg.info("realm: " + this._realm);

    this._digest_uri = "xmpp/" + this.domain;
    this._cnonce = JSJaCUtils.cnonce(14);
    this._nc = '00000001';

    var X = this.username+':'+this._realm+':'+this.pass;
    var Y = Crypt.rstr_md5(Crypt.str2rstr_utf8(X));

    var A1 = Y+':'+this._nonce+':'+this._cnonce;
    if (this.authzid) {
      A1 = A1 + ':' + this.authzid;
    }
    var HA1 = Crypt.rstr2hex(Crypt.rstr_md5(A1));

    var A2 = 'AUTHENTICATE:'+this._digest_uri;
    var HA2 = Crypt.hex_md5(A2);

    var response = Crypt.hex_md5(HA1+':'+this._nonce+':'+this._nc+':'+
                           this._cnonce+':auth:'+HA2);

    var rPlain = 'username="'+this.username+'",realm="'+this._realm+
    '",nonce="'+this._nonce+'",cnonce="'+this._cnonce+'",nc='+this._nc+
    ',qop=auth,digest-uri="'+this._digest_uri+'",response='+response+
    ',charset=utf-8';

    if (this.authzid) {
      rPlain = 'authzid="' + this.authzid + '",' + rPlain;
    }

    this.oDbg.info("response: "+rPlain);

    this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"+
                  Crypt.b64encode(rPlain)+"</response>",
                  this._doSASLAuthDigestMd5S2);
  }
};

/**
 * @private
 */
protected _doSASLAuthDigestMd5S2(el):void {
  if (el.nodeName == 'failure') {
    if (el.xml)
      this.oDbg.warn("auth error: "+el.xml);
    else
      this.oDbg.error("auth error");
    this._handleEvent('onerror',new JSJaCError('401','auth','not-authorized'));
    this.disconnect();
    return;
  }

  var response = Crypt.b64decode(el.firstChild.nodeValue);
  this.oDbg.info("response: "+response);

  var rspauth = response.substring(response.indexOf("rspauth=")+8);
  this.oDbg.info("rspauth: "+rspauth);

  var X = this.username+':'+this._realm+':'+this.pass;
  var Y = Crypt.rstr_md5(Crypt.str2rstr_utf8(X));

  var A1 = Y+':'+this._nonce+':'+this._cnonce;
  if (this.authzid) {
    A1 = A1 + ':' + this.authzid;
  }
  var HA1 = Crypt.rstr2hex(Crypt.rstr_md5(A1));

  var A2 = ':'+this._digest_uri;
  var HA2 = Crypt.hex_md5(A2);

  var rsptest = Crypt.hex_md5(HA1+':'+this._nonce+':'+this._nc+':'+
                        this._cnonce+':auth:'+HA2);
  this.oDbg.info("rsptest: "+rsptest);

  if (rsptest != rspauth) {
    this.oDbg.warn("SASL Digest-MD5: server repsonse with wrong rspauth");
    this.disconnect();
    return;
  }

    if (el.nodeName == 'success') {
        this._reInitStream(JSJaC.bind(this._doStreamBind, this));
    } else { // some extra turn
        this._sendRaw("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>",
                      this._doSASLAuthDone);
    }
};


/**
 * @private
 */
private _doStreamBind() {
  var iq = new JSJaCIQ();
  iq.setIQ(null,'set','bind_1');
  iq.appendNode("bind", {xmlns: NS_BIND}, [["resource", this.resource]]);
  this.oDbg.debug(iq.xml());
  this.send(iq,this._doXMPPSess);
};
/**
 * @private
 */
private _doXMPPSess(iq:JSJaCIQ):void {
  if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
    this.disconnect();
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    return;
  }

  this.fulljid = iq.getChildVal("jid");
  this.jid = this.fulljid.substring(0,this.fulljid.lastIndexOf('/'));

  iq = new JSJaCIQ();
  iq.setIQ(null,'set','sess_1');
  iq.appendNode("session", {xmlns: NS_SESSION}, []);
  this.oDbg.info(iq.xml());
  this.send(iq,this._doXMPPSessDone);
};

/**
 * @private
 */
private _doXMPPSessDone(iq:JSJaCIQ):void {
  if (iq.getType() != 'result' || iq.getType() == 'error') { // failed
    this.disconnect();
    if (iq.getType() == 'error')
      this._handleEvent('onerror',iq.getChild('error'));
    return;
  } else
    this._handleEvent('onconnect');
};



/**
 * 
 */
private _handleResponse(req):void {
  var rootEl = this._parseResponse(req);

  if (!rootEl)
    return;
/**
 *  becase parseReponse is return null so not used
 * 
  for (var i=0; i<rootEl.childNodes.length; i++) {
    if (this._sendRawCallbacks.length) {
      var cb = this._sendRawCallbacks[0];
      this._sendRawCallbacks = this._sendRawCallbacks.slice(1, this._sendRawCallbacks.length);
      cb.fn.call(this, rootEl.childNodes.item(i), cb.arg);
      continue;
    }
    this._inQ = this._inQ.concat(rootEl.childNodes.item(i));
  }
 */
};


/**
 * @private
 */
protected _parseStreamFeatures(doc:IXMLDocument):boolean {
    if (!doc) {
        this.oDbg.warn("nothing to parse ... aborting");
        return false;
    }
     /*
    var errorTag, i;
    if (doc.getElementsByTagNameNS) {
        errorTag = doc.getElementsByTagNameNS(NS_STREAM, "error").item(0);
    } else {
        var errors = doc.getElementsByTagName("error");
        for (i=0; i<errors.length; i++)
            if (errors.item(i).namespaceURI == NS_STREAM ||
                errors.item(i).getAttribute('xmlns') == NS_STREAM) {
                errorTag = errors.item(i);
                break;
            }
    }
    */

    if (doc.hasErrorTag()) {
        this._setStatus("internal_server_error");
        clearTimeout(this._timeout); // remove timer
        clearInterval(this._interval);
        clearInterval(this._inQto);
        this._handleEvent('onerror',new JSJaCError('503','cancel','session-terminate'));
        this._connected = false;
        this.oDbg.info("Disconnected.");
        this._handleEvent('ondisconnect');
        return false;
    }

    this.mechs = {};
    let lMec1 = doc.getElementsByTagName("mechanisms");
    if (!lMec1.length) return false;
    this.has_sasl = false;
    let i;
    for (i=0; i<lMec1.length; i++)
        if (lMec1[i].getAttribute("xmlns") == NS_SASL) {
            this.has_sasl=true;
            var lMec2 = lMec1[i].getElementsByTagName("mechanism");
            for (var j=0; j<lMec2.length; j++)
                this.mechs[lMec2[j].getfirstChildNodeValue()] = true;
            break;
        }
    if (this.has_sasl)
        this.oDbg.info("SASL detected");
    else {
        this.oDbg.warn("No support for SASL detected");
        return true;
    }

    /* [TODO]
     * check if in-band registration available
     * check for session and bind features
     */

    return true;
};




/**
 * Partial function binding sendEmpty to callback
 * @private
 */
private _prepSendEmpty(cb:Function, ctx:any):Function {
    return function() {
        ctx._sendEmpty(JSJaC.bind(cb, ctx));
    };
};
/**
 * send empty request
 * waiting for stream id to be able to proceed with authentication
 * @private
 */
private _sendEmpty(cb:Function):void {
  var slot = this._getFreeSlot();
  this._req[slot] = this._setupRequest(true);

  this._req[slot].r.onreadystatechange =
  JSJaC.bind(function() {
               if (this._req[slot].r.readyState == 4) {
                 this.oDbg.info("async recv: "+this._req[slot].r.responseText);
                   cb(this._req[slot].r); // handle response
               }
             },this);

  if (typeof(this._req[slot].r.onerror) != 'undefined') {
    this._req[slot].r.onerror =
      JSJaC.bind(function() {
                   this.oDbg.warn('XmlHttpRequest error');
                 }, this);
  }

  let reqstr:string = this._getRequestString('',false);
  this.oDbg.debug("sending: " + reqstr);
  this._req[slot].r.send(reqstr);
};


}
